﻿using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using FirstFloor.Documents.Annotations;
using FirstFloor.Documents.Annotations.Storage;
using FirstFloor.Documents.Controls;
using FirstFloor.Documents.Controls.Commands;

namespace FirstFloor.Documents.Presentation
{
    /// <summary>
    /// Represents a document viewmodel exposing a set of bindable document properties with support for text selection and annotations.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The document viewmodel automatically loads and unloads pages. The default implementation only has one page loaded at any time.
    /// </para>
    /// <para>
    /// Although using DependencyObject as base class in a view model is not conform MVVM principles, this is required for annotation purposes.
    /// </para>
    /// </remarks>
    public class DocumentViewModel
        : DependencyObject, ITextContainer, INotifyPropertyChanged
    {
        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        private bool canGoToNextPage = false;
        private bool canGoToPreviousPage = false;
        private GenericCommand<object> gotoNextPageCommand;
        private GenericCommand<object> gotoPreviousPageCommand;

        private int pageCount = 0;
        private int pageNumber = 0;
        private int pageIndex = -1;

        /// <summary>
        /// Initializes a new instance of the <see cref="DocumentViewModel"/> class.
        /// </summary>
        public DocumentViewModel()
            : this(new DocumentDataSource())
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DocumentViewModel"/> class.
        /// </summary>
        /// <param name="dataSource">The data source.</param>
        public DocumentViewModel(DocumentDataSource dataSource)
        {
            if (dataSource == null) {
                throw new ArgumentNullException("dataSource");
            }
            // create pages collection
            this.DataSource = dataSource;
            this.Pages = this.DataSource.CreatePageCollection();
            this.Pages.CollectionChanged += OnPageCollectionChanged;

            // create annotation store
            this.Annotations = new AnnotationStore();

            // enable annotations
            var service = new AnnotationService(this);
            service.Enable(this.Annotations);

            // create text selection
            this.Selection = new TextSelection {
                IsEnabled = true,
                TextContainer = this
            };

            // create commands
            this.gotoNextPageCommand = new GenericCommand<object>(o => GoToNextPage(), o => CanGoToNextPage);
            this.gotoPreviousPageCommand = new GenericCommand<object>(o => GoToPreviousPage(), o => CanGoToPreviousPage);
        }

        private void OnPageCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            // set the text container for each page
            foreach (var page in this.Pages.OfType<PageContext>()) {
                page.TextContainer = this;
            }
            this.PageCount = this.Pages.Count();
        }

        private void ChangePage(int pageIndex)
        {
            this.pageNumber = 0;
            this.pageIndex = -1;

            // find the page for given index
            var page = this.Pages.ElementAtOrDefault(pageIndex);
            if (page != null) {
                this.pageNumber = page.PageContent.PageNumber;
                this.pageIndex = pageIndex;
            }

            var oldPage = this.Page;
            this.Page = page;

            OnPropertyChanged("PageNumber");
            OnPropertyChanged("PageIndex");
            OnPropertyChanged("Page");

            OnPageChanged(oldPage, page);

            // update command state
            SetPageNavigationCommandStates();
        }

        /// <summary>
        /// Raises the PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Sets the page navigation command states.
        /// </summary>
        protected virtual void SetPageNavigationCommandStates()
        {
            this.CanGoToNextPage = this.pageIndex < this.pageCount && this.pageCount != 0;
            this.CanGoToPreviousPage = this.pageIndex > 0;
        }

        /// <summary>
        /// Called before the current page is changing.
        /// </summary>
        /// <param name="pageIndex">Index of the new page.</param>
        protected virtual void OnPageChanging(ref int pageIndex)
        {
        }

        /// <summary>
        /// Called when the current page has changed
        /// </summary>
        /// <param name="oldPage">The old page.</param>
        /// <param name="newPage">The new page.</param>
        protected virtual void OnPageChanged(PageContextBase oldPage, PageContextBase newPage)
        {
            // unload old page
            if (oldPage != null) {
                oldPage.IsInUse = false;
            }

            // load new page
            if (newPage != null) {
                newPage.IsInUse = true;
            }
        }

        /// <summary>
        /// Gets the annotations.
        /// </summary>
        /// <value>The annotations.</value>
        public AnnotationStore Annotations { get; private set; }
        /// <summary>
        /// Gets the document data source.
        /// </summary>
        /// <value>The data source.</value>
        public DocumentDataSource DataSource { get; private set; }
        /// <summary>
        /// Gets the text selection state.
        /// </summary>
        /// <value>The selection.</value>
        public TextSelection Selection { get; private set; }
        /// <summary>
        /// Gets the current page.
        /// </summary>
        /// <value>The page.</value>
        public PageContextBase Page { get; private set; }
        /// <summary>
        /// Gets the collection of all pages.
        /// </summary>
        /// <value>The pages.</value>
        public VirtualizingPageCollection Pages { get; private set; }

        /// <summary>
        /// Gets or sets the current page number.
        /// </summary>
        /// <value>The page number.</value>
        public int PageNumber
        {
            get { return this.pageNumber; }
            set
            {
                if (this.pageNumber != value) {
                    // find the page index of specified page
                    var page = this.Pages.Select((p, i) => new {
                        PageNumber = p.PageContent.PageNumber,
                        Index = i
                    }).FirstOrDefault(o => o.PageNumber == value);

                    var index = -1;
                    if (page != null) {
                        index = page.Index;
                    }

                    this.PageIndex = index;
                }
            }
        }

        /// <summary>
        /// Gets or sets the index of the current page.
        /// </summary>
        /// <value>The index of the page.</value>
        public int PageIndex
        {
            get { return this.pageIndex; }
            set
            {
                if (this.pageIndex != value) {
                    // allow subclass to modify page index
                    OnPageChanging(ref value);

                    if (this.pageIndex != value) {
                        ChangePage(value);
                    }
                }
            }
        }

        /// <summary>
        /// Gets the page count.
        /// </summary>
        /// <value>The page count.</value>
        public int PageCount
        {
            get { return this.pageCount; }
            private set {
                if (this.pageCount != value) {
                    this.pageCount = value;
                    OnPropertyChanged("PageCount");
                }
            }
        }

        /// <summary>
        /// Gets the command that navigates to the next page.
        /// </summary>
        /// <returns></returns>
        public ICommand GoToNextPageCommand
        {
            get { return this.gotoNextPageCommand; }
        }
        /// <summary>
        /// Gets the command that navigates to the previous page.
        /// </summary>
        /// <returns></returns>
        public ICommand GoToPreviousPageCommand
        {
            get { return this.gotoPreviousPageCommand; }
        }

        /// <summary>
        /// Gets a value indicating whether navigation to the next page is possible.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if navigation is possible; otherwise, <c>false</c>.
        /// </value>
        public bool CanGoToNextPage
       {
            get { return this.canGoToNextPage; }
            protected set
            {
                if (this.canGoToNextPage != value) {
                    this.canGoToNextPage = value;
                    OnPropertyChanged("CanGoToNextPage");

                    // and update command state
                    this.gotoNextPageCommand.RaiseCanExecuteChanged();
                }
            }
        }

        /// <summary>
        /// Gets a value indicating whether navigation to the previous page is possible.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if navigation is possible; otherwise, <c>false</c>.
        /// </value>
        public bool CanGoToPreviousPage
        {
            get { return this.canGoToPreviousPage; }
            protected set
            {
                if (this.canGoToPreviousPage != value) {
                    this.canGoToPreviousPage = value;
                    OnPropertyChanged("CanGoToPreviousPage");

                    // and update command state
                    this.gotoPreviousPageCommand.RaiseCanExecuteChanged();
                }
            }
        }

        /// <summary>
        /// Navigates to the next page.
        /// </summary>
        public virtual void GoToNextPage()
        {
            if (CanGoToNextPage) {
                this.PageIndex++;
            }
        }

        /// <summary>
        /// Navigates to the previous page.
        /// </summary>
        public virtual void GoToPreviousPage()
        {
            if (CanGoToPreviousPage) {
                this.PageIndex--;
            }
        }


        /// <summary>
        /// Attempts to bring the specified position into view.
        /// </summary>
        /// <param name="position">The position.</param>
        public void BringIntoView(TextPointer position)
        {
            if (position == null) {
                throw new ArgumentNullException("position");
            }
            // just bring the page into view
            this.PageNumber = position.PageNumber;
        }

        /// <summary>
        /// Attempts to bring the specified link target into view.
        /// </summary>
        /// <param name="linkTarget">The link target.</param>
        public void BringIntoView(LinkTarget linkTarget)
        {
            if (linkTarget == null) {
                throw new ArgumentNullException("linkTarget");
            }
            this.PageNumber = linkTarget.Page.PageNumber;
        }

        /// <summary>
        /// Gets the plain text in given text range.
        /// </summary>
        /// <param name="position1">The position1.</param>
        /// <param name="position2">The position2.</param>
        /// <returns>The plain text in given range.</returns>
        public string GetText(ref TextPointer position1, ref TextPointer position2)
        {
            if (position1 == null) {
                throw new ArgumentNullException("position1");
            }
            if (position2 == null) {
                throw new ArgumentNullException("position2");
            }

            if (this.Page != null && this.Page.PageContent.Text != null) {
                var start = TextPointer.Min(position1, position2);
                var end = TextPointer.Max(position1, position2);

                // only supports text retrieval for the current page
                if (start.PageNumber == this.PageNumber && start.PageNumber == end.PageNumber) {
                    var pageText = this.Page.PageContent.Text;
                    var startIndex = Math.Min(pageText.Length - 1, start.TextIndex);
                    var endIndex = Math.Min(pageText.Length, end.TextIndex);

                    return pageText.Substring(startIndex, endIndex - startIndex);
                }
            }
            return null;
        }

        /// <summary>
        /// Notifies the text container of a change of the annotation service.
        /// </summary>
        public void OnAnnotationServiceChanged()
        {
            // nothing here
        }
    }
}
